From fcc1f554d6ae5169c6156ce33ea7d2a916992b2a Mon Sep 17 00:00:00 2001 From: Benjamin Otte Date: Mon, 19 Dec 2016 05:13:42 +0100 Subject: [PATCH] gsk: Add GskInsetShadowNode And again lots of shadow code gets copied to GSK. But we're now almost at a stage where widget-factory does not use cairo nodes anymore. --- docs/reference/gsk/gsk4-sections.txt | 1 + gsk/gskenums.h | 2 + gsk/gskrendernode.h | 8 + gsk/gskrendernodeimpl.c | 469 +++++++++++++++++++++++++ gtk/gtkcssshadowvalue.c | 26 +- gtk/inspector/gtktreemodelrendernode.c | 1 + gtk/inspector/recorder.c | 2 + 7 files changed, 499 insertions(+), 10 deletions(-) diff --git a/docs/reference/gsk/gsk4-sections.txt b/docs/reference/gsk/gsk4-sections.txt index 8afc232452..9ddf205dad 100644 --- a/docs/reference/gsk/gsk4-sections.txt +++ b/docs/reference/gsk/gsk4-sections.txt @@ -37,6 +37,7 @@ gsk_linear_gradient_node_new gsk_repeating_linear_gradient_node_new gsk_border_node_new gsk_texture_node_new +gsk_inset_shadow_node_new gsk_cairo_node_new gsk_cairo_node_get_draw_context gsk_container_node_new diff --git a/gsk/gskenums.h b/gsk/gskenums.h index 9defa36653..1653654ff1 100644 --- a/gsk/gskenums.h +++ b/gsk/gskenums.h @@ -33,6 +33,7 @@ * linear gradient * @GSK_BORDER_NODE: A node stroking a border around an area * @GSK_TEXTURE_NODE: A node drawing a #GskTexture + * @GSK_INSET_SHADOW_NODE: A node drawing an inset shadow * @GSK_TRANSFORM_NODE: A node that renders its child after applying a * matrix transform * @GSK_OPACITY_NODE: A node that changes the opacity of its child @@ -55,6 +56,7 @@ typedef enum { GSK_REPEATING_LINEAR_GRADIENT_NODE, GSK_BORDER_NODE, GSK_TEXTURE_NODE, + GSK_INSET_SHADOW_NODE, GSK_TRANSFORM_NODE, GSK_OPACITY_NODE, GSK_CLIP_NODE, diff --git a/gsk/gskrendernode.h b/gsk/gskrendernode.h index 1c4620737b..6fc8724626 100644 --- a/gsk/gskrendernode.h +++ b/gsk/gskrendernode.h @@ -88,6 +88,14 @@ GskRenderNode * gsk_border_node_new (const GskRounde const float border_width[4], const GdkRGBA border_color[4]); +GDK_AVAILABLE_IN_3_90 +GskRenderNode * gsk_inset_shadow_node_new (const GskRoundedRect *outline, + const GdkRGBA *color, + float dx, + float dy, + float spread, + float blur_radius); + GDK_AVAILABLE_IN_3_90 GskRenderNode * gsk_cairo_node_new (const graphene_rect_t *bounds); GDK_AVAILABLE_IN_3_90 diff --git a/gsk/gskrendernodeimpl.c b/gsk/gskrendernodeimpl.c index a7453c6e88..9a9e6cdd6e 100644 --- a/gsk/gskrendernodeimpl.c +++ b/gsk/gskrendernodeimpl.c @@ -565,6 +565,475 @@ gsk_texture_node_new (GskTexture *texture, return &self->render_node; } +/*** GSK_INSET_SHADOW_NODE ***/ + +typedef struct _GskInsetShadowNode GskInsetShadowNode; + +struct _GskInsetShadowNode +{ + GskRenderNode render_node; + + GskRoundedRect outline; + GdkRGBA color; + float dx; + float dy; + float spread; + float blur_radius; +}; + +static void +gsk_inset_shadow_node_finalize (GskRenderNode *node) +{ +} + +static void +gsk_inset_shadow_node_make_immutable (GskRenderNode *node) +{ +} + +static gboolean +has_empty_clip (cairo_t *cr) +{ + double x1, y1, x2, y2; + + cairo_clip_extents (cr, &x1, &y1, &x2, &y2); + return x1 == x2 && y1 == y2; +} + +static void +draw_shadow (cairo_t *cr, + gboolean inset, + GskRoundedRect *box, + GskRoundedRect *clip_box, + float radius, + const GdkRGBA *color, + GskBlurFlags blur_flags) +{ + cairo_t *shadow_cr; + gboolean do_blur; + + if (has_empty_clip (cr)) + return; + + gdk_cairo_set_source_rgba (cr, color); + do_blur = (blur_flags & (GSK_BLUR_X | GSK_BLUR_Y)) != 0; + if (do_blur) + shadow_cr = gsk_cairo_blur_start_drawing (cr, radius, blur_flags); + else + shadow_cr = cr; + + cairo_set_fill_rule (shadow_cr, CAIRO_FILL_RULE_EVEN_ODD); + gsk_rounded_rect_path (box, shadow_cr); + if (inset) + cairo_rectangle (cr, + clip_box->bounds.origin.x, clip_box->bounds.origin.y, + clip_box->bounds.size.width, clip_box->bounds.size.height); + + cairo_fill (shadow_cr); + + if (do_blur) + gsk_cairo_blur_finish_drawing (shadow_cr, radius, color, blur_flags); +} + +typedef struct { + float radius; + graphene_size_t corner; +} CornerMask; + +typedef enum { + TOP, + RIGHT, + BOTTOM, + LEFT +} Side; + +static guint +corner_mask_hash (CornerMask *mask) +{ + return ((guint)mask->radius << 24) ^ + ((guint)(mask->corner.width*4)) << 12 ^ + ((guint)(mask->corner.height*4)) << 0; +} + +static gboolean +corner_mask_equal (CornerMask *mask1, + CornerMask *mask2) +{ + return + mask1->radius == mask2->radius && + mask1->corner.width == mask2->corner.width && + mask1->corner.height == mask2->corner.height; +} + +static void +draw_shadow_corner (cairo_t *cr, + gboolean inset, + GskRoundedRect *box, + GskRoundedRect *clip_box, + float radius, + const GdkRGBA *color, + GskCorner corner, + cairo_rectangle_int_t *drawn_rect) +{ + float clip_radius; + int x1, x2, x3, y1, y2, y3, x, y; + GskRoundedRect corner_box; + cairo_t *mask_cr; + cairo_surface_t *mask; + cairo_pattern_t *pattern; + cairo_matrix_t matrix; + float sx, sy; + static GHashTable *corner_mask_cache = NULL; + float max_other; + CornerMask key; + gboolean overlapped; + + clip_radius = gsk_cairo_blur_compute_pixels (radius); + + overlapped = FALSE; + if (corner == GSK_CORNER_TOP_LEFT || corner == GSK_CORNER_BOTTOM_LEFT) + { + x1 = floor (box->bounds.origin.x - clip_radius); + x2 = ceil (box->bounds.origin.x + box->corner[corner].width + clip_radius); + x = x1; + sx = 1; + max_other = MAX(box->corner[GSK_CORNER_TOP_RIGHT].width, box->corner[GSK_CORNER_BOTTOM_RIGHT].width); + x3 = floor (box->bounds.origin.x + box->bounds.size.width - max_other - clip_radius); + if (x2 > x3) + overlapped = TRUE; + } + else + { + x1 = floor (box->bounds.origin.x + box->bounds.size.width - box->corner[corner].width - clip_radius); + x2 = ceil (box->bounds.origin.x + box->bounds.size.width + clip_radius); + x = x2; + sx = -1; + max_other = MAX(box->corner[GSK_CORNER_TOP_LEFT].width, box->corner[GSK_CORNER_BOTTOM_LEFT].width); + x3 = ceil (box->bounds.origin.x + max_other + clip_radius); + if (x3 > x1) + overlapped = TRUE; + } + + if (corner == GSK_CORNER_TOP_LEFT || corner == GSK_CORNER_TOP_RIGHT) + { + y1 = floor (box->bounds.origin.y - clip_radius); + y2 = ceil (box->bounds.origin.y + box->corner[corner].height + clip_radius); + y = y1; + sy = 1; + max_other = MAX(box->corner[GSK_CORNER_BOTTOM_LEFT].height, box->corner[GSK_CORNER_BOTTOM_RIGHT].height); + y3 = floor (box->bounds.origin.y + box->bounds.size.height - max_other - clip_radius); + if (y2 > y3) + overlapped = TRUE; + } + else + { + y1 = floor (box->bounds.origin.y + box->bounds.size.height - box->corner[corner].height - clip_radius); + y2 = ceil (box->bounds.origin.y + box->bounds.size.height + clip_radius); + y = y2; + sy = -1; + max_other = MAX(box->corner[GSK_CORNER_TOP_LEFT].height, box->corner[GSK_CORNER_TOP_RIGHT].height); + y3 = ceil (box->bounds.origin.y + max_other + clip_radius); + if (y3 > y1) + overlapped = TRUE; + } + + drawn_rect->x = x1; + drawn_rect->y = y1; + drawn_rect->width = x2 - x1; + drawn_rect->height = y2 - y1; + + cairo_rectangle (cr, x1, y1, x2 - x1, y2 - y1); + cairo_clip (cr); + + if (inset || overlapped) + { + /* Fall back to generic path if inset or if the corner radius + runs into each other */ + draw_shadow (cr, inset, box, clip_box, radius, color, GSK_BLUR_X | GSK_BLUR_Y); + return; + } + + if (has_empty_clip (cr)) + return; + + /* At this point we're drawing a blurred outset corner. The only + * things that affect the output of the blurred mask in this case + * is: + * + * What corner this is, which defines the orientation (sx,sy) + * and position (x,y) + * + * The blur radius (which also defines the clip_radius) + * + * The the horizontal and vertical corner radius + * + * We apply the first position and orientation when drawing the + * mask, so we cache rendered masks based on the blur radius and the + * corner radius. + */ + if (corner_mask_cache == NULL) + corner_mask_cache = g_hash_table_new_full ((GHashFunc)corner_mask_hash, + (GEqualFunc)corner_mask_equal, + g_free, (GDestroyNotify)cairo_surface_destroy); + + key.radius = radius; + key.corner = box->corner[corner]; + + mask = g_hash_table_lookup (corner_mask_cache, &key); + if (mask == NULL) + { + mask = cairo_surface_create_similar_image (cairo_get_target (cr), CAIRO_FORMAT_A8, + drawn_rect->width + clip_radius, + drawn_rect->height + clip_radius); + mask_cr = cairo_create (mask); + gsk_rounded_rect_init_from_rect (&corner_box, &GRAPHENE_RECT_INIT (clip_radius, clip_radius, 2*drawn_rect->width, 2*drawn_rect->height), 0); + corner_box.corner[0] = box->corner[corner]; + gsk_rounded_rect_path (&corner_box, mask_cr); + cairo_fill (mask_cr); + gsk_cairo_blur_surface (mask, radius, GSK_BLUR_X | GSK_BLUR_Y); + cairo_destroy (mask_cr); + g_hash_table_insert (corner_mask_cache, g_memdup (&key, sizeof (key)), mask); + } + + gdk_cairo_set_source_rgba (cr, color); + pattern = cairo_pattern_create_for_surface (mask); + cairo_matrix_init_identity (&matrix); + cairo_matrix_scale (&matrix, sx, sy); + cairo_matrix_translate (&matrix, -x, -y); + cairo_pattern_set_matrix (pattern, &matrix); + cairo_mask (cr, pattern); + cairo_pattern_destroy (pattern); +} + +static void +draw_shadow_side (cairo_t *cr, + gboolean inset, + GskRoundedRect *box, + GskRoundedRect *clip_box, + float radius, + const GdkRGBA *color, + Side side, + cairo_rectangle_int_t *drawn_rect) +{ + GskBlurFlags blur_flags = GSK_BLUR_REPEAT; + gdouble clip_radius; + int x1, x2, y1, y2; + + clip_radius = gsk_cairo_blur_compute_pixels (radius); + + if (side == TOP || side == BOTTOM) + { + blur_flags |= GSK_BLUR_Y; + x1 = floor (box->bounds.origin.x - clip_radius); + x2 = ceil (box->bounds.origin.x + box->bounds.size.width + clip_radius); + } + else if (side == LEFT) + { + x1 = floor (box->bounds.origin.x -clip_radius); + x2 = ceil (box->bounds.origin.x + clip_radius); + } + else + { + x1 = floor (box->bounds.origin.x + box->bounds.size.width -clip_radius); + x2 = ceil (box->bounds.origin.x + box->bounds.size.width + clip_radius); + } + + if (side == LEFT || side == RIGHT) + { + blur_flags |= GSK_BLUR_X; + y1 = floor (box->bounds.origin.y - clip_radius); + y2 = ceil (box->bounds.origin.y + box->bounds.size.height + clip_radius); + } + else if (side == TOP) + { + y1 = floor (box->bounds.origin.y -clip_radius); + y2 = ceil (box->bounds.origin.y + clip_radius); + } + else + { + y1 = floor (box->bounds.origin.y + box->bounds.size.height -clip_radius); + y2 = ceil (box->bounds.origin.y + box->bounds.size.height + clip_radius); + } + + drawn_rect->x = x1; + drawn_rect->y = y1; + drawn_rect->width = x2 - x1; + drawn_rect->height = y2 - y1; + + cairo_rectangle (cr, x1, y1, x2 - x1, y2 - y1); + cairo_clip (cr); + draw_shadow (cr, inset, box, clip_box, radius, color, blur_flags); +} + +static gboolean +needs_blur (GskInsetShadowNode *self) +{ + /* The code doesn't actually do any blurring for radius 1, as it + * ends up with box filter size 1 */ + if (self->blur_radius <= 1.0) + return FALSE; + + return TRUE; +} + +static void +gsk_inset_shadow_node_draw (GskRenderNode *node, + cairo_t *cr) +{ + GskInsetShadowNode *self = (GskInsetShadowNode *) node; + GskRoundedRect box, clip_box; + int clip_radius; + double x1c, y1c, x2c, y2c; + + /* We don't need to draw invisible shadows */ + if (gdk_rgba_is_clear (&self->color)) + return; + + cairo_clip_extents (cr, &x1c, &y1c, &x2c, &y2c); + if (!gsk_rounded_rect_intersects_rect (&self->outline, &GRAPHENE_RECT_INIT (x1c, y1c, x2c - x1c, y2c - y1c))) + return; + + clip_radius = gsk_cairo_blur_compute_pixels (self->blur_radius); + + cairo_save (cr); + + gsk_rounded_rect_path (&self->outline, cr); + cairo_clip (cr); + + gsk_rounded_rect_init_copy (&box, &self->outline); + gsk_rounded_rect_offset (&box, self->dx, self->dy); + gsk_rounded_rect_shrink (&box, self->spread, self->spread, self->spread, self->spread); + + gsk_rounded_rect_init_copy (&clip_box, &self->outline); + gsk_rounded_rect_shrink (&clip_box, -clip_radius, -clip_radius, -clip_radius, -clip_radius); + + if (!needs_blur (self)) + draw_shadow (cr, TRUE, &box, &clip_box, self->blur_radius, &self->color, GSK_BLUR_NONE); + else + { + cairo_region_t *remaining; + cairo_rectangle_int_t r; + int i; + + /* For the blurred case we divide the rendering into 9 parts, + * 4 of the corners, 4 for the horizonat/vertical lines and + * one for the interior. We make the non-interior parts + * large enought to fit the full radius of the blur, so that + * the interior part can be drawn solidly. + */ + + /* In the inset case we want to paint the whole clip-box. + * We could remove the part of "box" where the blur doesn't + * reach, but computing that is a bit tricky since the + * rounded corners are on the "inside" of it. */ + r.x = floor (clip_box.bounds.origin.x); + r.y = floor (clip_box.bounds.origin.y); + r.width = ceil (clip_box.bounds.origin.x + clip_box.bounds.size.width) - r.x; + r.height = ceil (clip_box.bounds.origin.y + clip_box.bounds.size.height) - r.y; + remaining = cairo_region_create_rectangle (&r); + + /* First do the corners of box */ + for (i = 0; i < 4; i++) + { + cairo_save (cr); + /* Always clip with remaining to ensure we never draw any area twice */ + gdk_cairo_region (cr, remaining); + cairo_clip (cr); + draw_shadow_corner (cr, TRUE, &box, &clip_box, self->blur_radius, &self->color, i, &r); + cairo_restore (cr); + + /* We drew the region, remove it from remaining */ + cairo_region_subtract_rectangle (remaining, &r); + } + + /* Then the sides */ + for (i = 0; i < 4; i++) + { + cairo_save (cr); + /* Always clip with remaining to ensure we never draw any area twice */ + gdk_cairo_region (cr, remaining); + cairo_clip (cr); + draw_shadow_side (cr, TRUE, &box, &clip_box, self->blur_radius, &self->color, i, &r); + cairo_restore (cr); + + /* We drew the region, remove it from remaining */ + cairo_region_subtract_rectangle (remaining, &r); + } + + /* Then the rest, which needs no blurring */ + + cairo_save (cr); + gdk_cairo_region (cr, remaining); + cairo_clip (cr); + draw_shadow (cr, TRUE, &box, &clip_box, self->blur_radius, &self->color, GSK_BLUR_NONE); + cairo_restore (cr); + + cairo_region_destroy (remaining); + } + + cairo_restore (cr); +} + +static void +gsk_inset_shadow_node_get_bounds (GskRenderNode *node, + graphene_rect_t *bounds) +{ + GskInsetShadowNode *self = (GskInsetShadowNode *) node; + + graphene_rect_init_from_rect (bounds, &self->outline.bounds); +} + +static const GskRenderNodeClass GSK_INSET_SHADOW_NODE_CLASS = { + GSK_INSET_SHADOW_NODE, + sizeof (GskInsetShadowNode), + "GskInsetShadowNode", + gsk_inset_shadow_node_finalize, + gsk_inset_shadow_node_make_immutable, + gsk_inset_shadow_node_draw, + gsk_inset_shadow_node_get_bounds +}; + +/** + * gsk_inset_shadow_node_new: + * @outline: outline of the region containing the shadow + * @color: color of the shadow + * @dx: horizontal offset of shadow + * @dy: vertical offset of shadow + * @spread: how far the shadow spreads towards the inside + * @blur_radius: how much blur to apply to the shadow + * + * Creates a #GskRenderNode that will render an inset shadow + * into the box given by @outline. + * + * Returns: A new #GskRenderNode + * + * Since: 3.90 + */ +GskRenderNode * +gsk_inset_shadow_node_new (const GskRoundedRect *outline, + const GdkRGBA *color, + float dx, + float dy, + float spread, + float blur_radius) +{ + GskInsetShadowNode *self; + + g_return_val_if_fail (outline != NULL, NULL); + g_return_val_if_fail (color != NULL, NULL); + + self = (GskInsetShadowNode *) gsk_render_node_new (&GSK_INSET_SHADOW_NODE_CLASS); + + gsk_rounded_rect_init_copy (&self->outline, outline); + self->color = *color; + self->dx = dx; + self->dy = dy; + self->spread = spread; + self->blur_radius = blur_radius; + + return &self->render_node; +} + /*** GSK_CAIRO_NODE ***/ typedef struct _GskCairoNode GskCairoNode; diff --git a/gtk/gtkcssshadowvalue.c b/gtk/gtkcssshadowvalue.c index f132bfc990..91b24bf200 100644 --- a/gtk/gtkcssshadowvalue.c +++ b/gtk/gtkcssshadowvalue.c @@ -1066,7 +1066,9 @@ gtk_css_shadow_value_snapshot_inset (const GtkCssValue *shadow, GtkSnapshot *snapshot, const GskRoundedRect*padding_box) { - cairo_t *cr; + GskRoundedRect outline; + GskRenderNode *node; + double off_x, off_y; g_return_if_fail (shadow->class == >K_CSS_VALUE_SHADOW); @@ -1074,14 +1076,18 @@ gtk_css_shadow_value_snapshot_inset (const GtkCssValue *shadow, if (gdk_rgba_is_clear (_gtk_css_rgba_value_get_rgba (shadow->color))) return; - cr = gtk_snapshot_append_cairo_node (snapshot, - &GRAPHENE_RECT_INIT ( - padding_box->bounds.origin.x, - padding_box->bounds.origin.y, - padding_box->bounds.size.width, - padding_box->bounds.size.height), - "Inset Shadow"); - _gtk_css_shadow_value_paint_box (shadow, cr, padding_box); - cairo_destroy (cr); + gtk_snapshot_get_offset (snapshot, &off_x, &off_y); + gsk_rounded_rect_init_copy (&outline, padding_box); + gsk_rounded_rect_offset (&outline, off_x, off_y); + + node = gsk_inset_shadow_node_new (&outline, + _gtk_css_rgba_value_get_rgba (shadow->color), + _gtk_css_number_value_get (shadow->hoffset, 0), + _gtk_css_number_value_get (shadow->voffset, 0), + _gtk_css_number_value_get (shadow->spread, 0), + _gtk_css_number_value_get (shadow->radius, 0)); + gsk_render_node_set_name (node, "Inset Shadow"); + gtk_snapshot_append_node (snapshot, node); + gsk_render_node_unref (node); } diff --git a/gtk/inspector/gtktreemodelrendernode.c b/gtk/inspector/gtktreemodelrendernode.c index 6064f9c431..32177fa813 100644 --- a/gtk/inspector/gtktreemodelrendernode.c +++ b/gtk/inspector/gtktreemodelrendernode.c @@ -528,6 +528,7 @@ append_node (GtkTreeModelRenderNode *nodemodel, case GSK_LINEAR_GRADIENT_NODE: case GSK_REPEATING_LINEAR_GRADIENT_NODE: case GSK_BORDER_NODE: + case GSK_INSET_SHADOW_NODE: /* no children */ break; diff --git a/gtk/inspector/recorder.c b/gtk/inspector/recorder.c index d1b4117556..23f6d86d65 100644 --- a/gtk/inspector/recorder.c +++ b/gtk/inspector/recorder.c @@ -151,6 +151,8 @@ node_type_name (GskRenderNodeType type) return "Border"; case GSK_TEXTURE_NODE: return "Texture"; + case GSK_INSET_SHADOW_NODE: + return "Inset Shadow"; case GSK_TRANSFORM_NODE: return "Transform"; case GSK_OPACITY_NODE: -- 2.30.2